--[[

Portal read and write business rules related functions 

Copyright 2014 Tiberiu CORBU
Authors: Tiberiu CORBU

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
--]]

if not element_portals then
	element_portals = {}
end

element_portals.registered_portals = {}
-- unused
element_portals.disabled_portal_keys = {}

-- builds a key form the position and the player instances. 
-- all i/o on portal data rely on this function  
function element_portals:construct_portal_id_key (pos, player, player_name)
	if player and not player_name then player_name = player:get_player_name() end
	local coords = minetest.pos_to_string(pos)
	return player_name..coords
end

function element_portals:get_portal_data_by_key(key, player, player_name)
	if player and not player_name then player_name = player:get_player_name() end
	local portals = element_portals:read_player_portals_table(player, player_name)
	return portals[key]
end

function element_portals:get_portal_data(pos, player, player_name)
	if player and not player_name then player_name = player:get_player_name() end
	local key = portal_key or element_portals:construct_portal_id_key(pos, player_name)
	return element_portals:get_portal_data_by_key(key)
end

function element_portals:remove_portal_data (pos, player)
	local portals = element_portals:read_player_portals_table(player)
	local key = element_portals:construct_portal_id_key(pos, player)
	if not pos or not player then
		return
	end 
	portals[key] = nil
	element_portals:write_player_portals_table(player, portals)
	minetest.chat_send_player(player:get_player_name(), "Portal removed from "..minetest.pos_to_string(pos)..".")
end

function element_portals:generate_portal_name(portals)
	local count = element_portals:tablelength(portals);
	local prefix = "Portal"
	return element_portals:generate_portal_name_with(prefix, count-1, portals, {})
end

function element_portals:table_contains(value, values) 
	local result = false
	if values and value then
		local _, check_value
		for _, check_value in pairs(values) do
			if check_value  == value then
				result = true
				break
			end
		end
	else 
		result = true 
	end
	return result
end

function element_portals:name_exists(name, portals, exclude_keys) 
	local name_exists = false
	local k, v
	for k, v in pairs(portals) do	
		if not element_portals:table_contains(k, exclude_keys) and v.portal_name == name then
			name_exists = true
			break
		end
	end
	return name_exists
end

function element_portals:generate_portal_name_with(prefix, count, portals, exclude_keys)
	local generated_name = prefix
	local name_exists = element_portals:name_exists(generated_name, portals, exclude_keys) 
	while name_exists do
		count = count + 1
		generated_name = prefix.." "..count
		name_exists = element_portals:name_exists(generated_name, portals, exclude_keys) 
	end
	return generated_name
end

function element_portals:create_portal(pos, player, node_name) 
	local name = player:get_player_name()
	local coords = minetest.pos_to_string(pos)
  if not node_name then node_name = minetest.get_node(pos) end
	if not player then
		minetest.chat_send_player(name, "Failed to create portal at "..coords..". Player is not set")
		return
	end
	local portals = element_portals:read_player_portals_table(player)
	local portal_name = element_portals:generate_portal_name(portals)
	local key = element_portals:construct_portal_id_key(pos, player)
	portals[key] = {pos = pos, portal_name = portal_name, node_name = node_name}
	element_portals:write_player_portals_table(player, portals)
	minetest.chat_send_player(name, "Portal added at "..coords..".")
	return portal_name
end

local set_meta_int = function(meta, key, value)
	local current_value = meta:get_int(key) 
	local value_to_set = value or 0
	if current_value ~= value_to_set then
		meta:set_int(key, value_to_set)
	end
end

local set_meta = function(meta, key, value)
	local current_value = meta:get_string(key) or "";
	local value_to_set = value or ""
		if current_value ~= value_to_set then
			meta:set_string(key, value_to_set)
		end

end

function element_portals:set_owner(meta, placer, portal_name, portal_is_private)
  local placer_name
  if placer then
    if portal_is_private then 
      set_meta(meta, "is_private", 'true')
    end
    placer_name = placer:get_player_name() or ''
    set_meta(meta, "placer", placer_name)
    set_meta(meta, "owner", placer_name)
  end
  if portal_name then
    local infotext = string.format("%s (owned by %s)", portal_name, placer_name or '')
    set_meta(meta, "infotext", infotext)
  end
  return placer_name or ''
end

function element_portals:set_portal_node_meta(meta, params, name)
	set_meta(meta, "fuel_surrounding", params.fuel_surrounding)
	set_meta_int(meta, "fuel_surrounding_count", params.fuel_surrounding_count)
	set_meta(meta, "fuel_stack", params.fuel_stack)
	set_meta(meta, "portal_node_name", name)
end

function element_portals:construct(meta, has_fuel)
    meta:set_string("infotext", "Private Portal")
		meta:set_string("owner", "")
		if has_fuel then
      local inv = meta:get_inventory()
      inv:set_size("fuel", 1)
  end
end

function element_portals:register_portal(portal_node_name, portal_params) 
	if portal_node_name and not element_portals.registered_portals[portal_node_name] then
		element_portals.registered_portals[portal_node_name] = portal_params
	end
end


function element_portals:is_registerd_portal(node_name)
	local k, v
	for k, v in pairs(element_portals.registered_portals) do
		if k == node_name then
			return true
		end
	end
	return false
end

function element_portals:get_portal_filter_group(node_name)
	local result = nil
	if element_portals.registered_portals[node_name] then
		local data = element_portals.registered_portals[node_name]
		result = data.filter_group
	end
	return result
end

function element_portals:get_portal_type(node_name)
	local result = nil
	if element_portals.registered_portals[node_name] then
		local data = element_portals.registered_portals[node_name]
		result = data.portal_type
	end
	return result
end

-- verifyes if the portal is registered (submod enabled) and is an out type 
function element_portals:is_registered_out_portal(node_name, group)
	
	if element_portals.registered_portals[node_name] then
	
		local data = element_portals.registered_portals[node_name]
		
		local out_type = data.portal_type == element_portals.IN_OUT_PORTAL 
				or data.portal_type == element_portals.OUT_PORTAL
		
		local in_group = element_portals:table_contains(group, data.portal_groups)
		
		if out_type and in_group then
			return true
		end
		
	else 
		minetest.log("action", node_name .." is not a registered portal")
	end
	return false
end

function element_portals:fix_portal_name(k, v, portals)
	return element_portals:generate_portal_name_with(v.portal_name or "Portal", 0, portals, {k})
end

function element_portals:sanitize_player_portals(player)
	local portals = element_portals:read_player_portals_table(player)
	local altered_portals
	local portal_keys_to_remove = {}
	local k, v
	for k, v in pairs(portals) do
		local fix_data_result = element_portals:fix_portal_data(k, v)
		if fix_data_result == element_portals.REMOVE_PORTAL_ACTION then
			 minetest.log("action", "Portal with key "..k.." was scheduled to be removed from user portal data")
			 table.insert(portal_keys_to_remove, k)
		end
		local new_name_result = element_portals:fix_portal_name(k, v, portals)
		if new_name_result ~= v.portal_name then 
			minetest.log("action", "Portal with key "..k.." has a duplicate name "..v.portal_name.. " renaming to "..new_name_result)
			v.portal_name = new_name_result
		end
		
	end
	local _, key
	for _, key in pairs(portal_keys_to_remove) do
		portals[key] = nil
	end
	element_portals:write_player_portals_table(player, portals)
	minetest.log("info", "Finishing portal sanitization")
end

function element_portals:disable_portal(portal_key)
	table.insert(element_portals.disabled_portal_keys, portal_key)
end

function element_portals:get_portal_node_data(portal_key, portal_params)
	local manip = minetest.get_voxel_manip()
	manip:read_from_map(portal_params.pos, portal_params.pos)
	local meta = minetest.get_meta(portal_params.pos)
	local node = minetest.get_node(portal_params.pos)
	return {meta = meta, node=node}
end

function element_portals:fix_portal_data(k, v)
 	local node_data = element_portals:get_portal_node_data(k, v)
	local meta = node_data.meta
	local node = node_data.node
	local node_name = node.name
	if not element_portals:is_registerd_portal(node_name) then
		minetest.log("action", "Portal with key "..k.." of type  ".. node_name.." is not registered") 
		--Clear Data from this portal
		--if node_name == 'air' then
			-- remove from data - but delegate to 
			return element_portals.REMOVE_PORTAL_ACTION
		-- else
			--[[ portal type is either disabled from game mod config, 
			 either it was replaced somehow without calling portal 
			 node on_desctruct impl.
			for now the node/data is checked
			 only on teleport function]]-- 
			 
			-- element_portals:disable_portal(k)
		--end
	else 	
		-- overwrite node meta
		minetest.log("action", "Portal with key "..k.." of type  ".. node_name.." is registered and the right type of node exists, setting meta on the node") 
		element_portals:set_portal_node_meta(meta, element_portals.registered_portals[node_name], node_name)
		-- overwrite portal data name 
		v.node_name = node_name
	end
	return element_portals.VOID_ACTION
end

-- Consumes the fuel, substracts the number of the stack specifiend in meta
-- @return true if the fuel was consumed, false otherwise 
function element_portals:consume_fuel(meta)
	local inv = meta:get_inventory()
	local fuel_stack = meta:get_string("fuel_stack")
	
	if inv:contains_item("fuel", fuel_stack) then
		-- exception for bucket with water, lava or anything else - be good and just empty the bucket
		
		local take_stack = ItemStack(fuel_stack)
		
		local inv_list = inv:get_list("fuel");
		local inv_stack = inv_list[1];
		-- 						   ^ - magic number that works
		inv_stack:take_item(take_stack:get_count())
		-- mintest support about stacks is poor documented -- any simple solution ?  
		if element_portals:string_starts(fuel_stack, "bucket:bucket") and inv_stack:get_count() == 0 then
			inv:set_list("fuel", {"bucket:bucket_empty 1"}) 
		else 
			inv:set_list("fuel", {inv_stack})
		end	 
		return true
	else 
		return false
	end
end


function element_portals:teleport_to_portal(player, portal_key, portal_data, travel_free, departure_meta, departure_node_name)

  if not player and portal_key and portal_data then return end
  local destination_node_data = element_portals:get_portal_node_data(portal_key, portal_data)
  
  local valid_end_point_portal =  element_portals:is_registered_out_portal(destination_node_data.node.name)
  if valid_end_point_portal then
    local teleport_posible
    if not travel_free then
      teleport_posible = (departure_meta and element_portals:consume_fuel(departure_meta, player))
    else 
      teleport_posible = travel_free
    end
    
    if teleport_posible then
      
      if departure_node_name then
        element_portals:play_node_action_sound(element_portals.TELEPORT_ACTION, departure_node_name, player)
      end

      local target_pos = { x = portal_data.pos.x, y = portal_data.pos.y + 1, z = portal_data.pos.z }
      player:set_pos(target_pos)
    end
  else
    minetest.chat_send_player(player:get_player_name(), "The selected portal does not exists or is disabled and it cannot be used")
  end
  
end

function element_portals:teleport_to(selected_portal_name, player, travel_free, departure_meta_and_pos)

	local portals = {}
	if not departure_meta_and_pos then departure_meta_and_pos = {} end

  local portal_placer_name, portal_placer
	local departure_meta = departure_meta_and_pos.meta
	local departure_pos = departure_meta_and_pos.pos
	local departure_node_name = departure_meta_and_pos.node_name
  
	if departure_pos and not departure_meta then
    departure_meta = minetest.get_meta(pos)
  end

  if departure_meta then
    for _,key in ipairs({'owner','placer','portal_placer'}) do
      portal_placer_name = departure_meta:get(key) 
      if portal_placer_name and portal_placer_name ~= "" then break end
    end
  end
  -- if not placer_name then placer_name = player:get_player_name() end
  if portal_placer_name then
    portal_placer = minetest.get_player_by_name(portal_placer_name)
  else
    if player then
      minetest.chat_send_player(player:get_player_name(), "Unable to select this portal's owner")
    end
    return
  end

  portals = element_portals:read_player_portals_table(portal_placer, portal_placer_name)
  
  if departure_pos then
    local departure_portal_key = element_portals:construct_portal_id_key (departure_pos, portal_placer, portal_placer_name)
    
    if portals[departure_portal_key] then
      departure_node_name = portals[departure_portal_key].node_name
    end
  end
  
	for k,v in pairs(portals) do
		  if v.portal_name == selected_portal_name then
        element_portals:teleport_to_portal(player, k, v, travel_free, departure_meta, departure_node_name)
      end
	end
  
end

function element_portals:teleport_to_portal_at_pos(destination, player, destination_placer_name, travel_free, departure_meta_and_pos)
  if not destination then return end
  local dep = departure_meta_and_pos or {}
  local portal_key, portal_data
  if not destination_placer_name then 
    local destination_meta =  minetest.get_meta(destination)
    destination_placer_name = destination_meta:get("owner") or destination_meta:get("placer") or destination_meta:get("portal_placer")
  end

  local destination_placer = minetest.get_player_by_name(destination_placer_name)
  portal_key = element_portals:construct_portal_id_key(destination, destination_placer, destination_placer_name)
  portal_data = element_portals:get_portal_data_by_key(portal_key, destination_placer, destination_placer_name)
    
  if portal_key and portal_data then
    element_portals:teleport_to_portal(player, portal_key, portal_data, travel_free, dep.meta, dep.node_name)
  end
end


function element_portals:build_cave(pos,player)
	local player_name = player:get_player_name()

	-- Re-check that pos is not air
	local na = minetest.get_node(pos)
	local nb = minetest.get_node({x=pos.x,y=pos.y+1,z=pos.z})
	minetest.log("action", "Cave construction check at "..minetest.pos_to_string(pos))	
	if na.name == "air" and nb.name == "air" then return 
	elseif na.name == "ignore" or nb.name == "ignore" then 
		minetest.log("action",
			"Téleporting of '"..player_name.."' was too fast, node name is '"..nptd.name.."'... landing cave could not be digged at"..minetest.pos_to_string(pos_to_dig))
		return
	end
	minetest.log("action", "Cave construction confirmed at "..minetest.pos_to_string(pos))
	
	-- Build the cave
	for ix=0,2 do
		for iz=0,2 do
			for iy=0,2 do
				local pos_to_dig = {x=pos.x+ix,y=pos.y+iy,z=pos.z+iz}
				local nptd = minetest.get_node(pos_to_dig)
				-- dig node
				if nptd and nptd.name ~= "air" and nptd.name ~= "ignore" then
					minetest.log("action", "Digging landing Cave at "..minetest.pos_to_string(pos_to_dig))	
					minetest.dig_node(pos_to_dig)
				end
			end
		end
	end
	-- Place a torch
	local tp = {x=pos.x+1,y=pos.y+2,z=pos.z+2}
	local tpw = {x=pos.x+1,y=pos.y+2,z=pos.z+3}
	local tpwn = minetest.get_node(tpw)
	if tpwn and tpwn.name ~= "air" then 
		minetest.set_node(tp, {name="default:torch_wall", param2=4})
	end
end

function element_portals:teleport_landing_tricks(pos,player)
	local player_name = player:get_player_name()
	local pn = minetest.get_node(pos).name
	-- if not landing in air
	if pn and pn ~= "air" then
		minetest.log("action", player_name.." wasn't in air at "..minetest.pos_to_string(pos))
		local dig_landing_cave = false
		local count = 1
		local max_count = 50
		-- Search for air above
		while pn ~= "air" and count < max_count do
			pos.y=pos.y+1
			pn = minetest.get_node(pos).name
			count = count+1
			if count == max_count-1 or pn == "ignore" then
				-- After a while stop searching and look for air around
				local nnp = minetest.find_node_near(pos, 25, "air")
				if nnp ~= nil then
					pos = nnp
					minetest.log("action", "Air found at "..minetest.pos_to_string(pos))
					-- Make sure there is at least 2 nodes of air
					nnpp = {x=pos.x,y=pos.y+1,z=pos.z}
					-- Or will dig a cave around if not
					local cn = minetest.get_node(nnpp).name
					if cn ~= "air" and minetest.get_node_group(cn, "water") < 1 then
						minetest.log("action","... but not at "..minetest.pos_to_string(nnpp))
						dig_landing_cave = true 
					end
				else
					minetest.log("action", "No air found around "..minetest.pos_to_string(pos))
					-- If air not found, return to inital landing pos	
					pos.y = 14
					pn = minetest.get_node(pos).name
					-- And will dig a cave around
					if minetest.get_node_group(pn, "water") < 1 then 
						minetest.log("action", "Will dig a cave for safe landing at "..minetest.pos_to_string(pos))
						dig_landing_cave = true 
					end
				end
			    break
			end
		end
		minetest.log("action", player_name.." will finally land at "..minetest.pos_to_string(pos))
		player:setpos(pos)
		if dig_landing_cave then
			minetest.after(1,function(pos,player)
				element_portals:build_cave(pos,player)
			end ,pos,player)
		end
	end
	return pos	
end 

function element_portals:teleport_random(player, departure_fields, departure_portal_meta_and_pos)
	local departure_meta_and_pos = departure_portal_meta_and_pos
	local departure_portal_meta 
	local departure_portal_pos
	local departure_portal_node_name 
	if departure_meta_and_pos then
		departure_portal_meta = departure_meta_and_pos.meta
		departure_portal_pos = departure_meta_and_pos.pos
	end 
	local portals = element_portals:read_player_portals_table(player)

	if departure_portal_pos then
		local departure_portal_key = element_portals:construct_portal_id_key (departure_portal_pos, player)
		if portals[departure_portal_key] then
		 departure_portal_node_name =  portals[departure_portal_key].node_name
		end
	end
	
	local dest_pos, dest_x, dest_z, dest_y, count
	-- Determine destination
	dest_x = math.random(-30912,30927)
	dest_z = math.random(-30912,30927)
	dest_y = 14
	count = 1
	
	-- Is teleport possible ?
	local travel_free = departure_fields["travel_free"] == "true"
	local teleport_possible
	if not travel_free then
		teleport_possible = (departure_portal_meta and element_portals:consume_fuel(departure_portal_meta, player))
	else 
		teleport_possible = travel_free
	end
	
	
	if teleport_possible then
		local dest_pos={x=dest_x,y=dest_y,z=dest_z}

		if departure_portal_node_name then
			element_portals:play_node_action_sound(element_portals.TELEPORT_ACTION, departure_portal_node_name , player)
		end
		player:setpos(dest_pos)
		minetest.after(1,function(player)
			local np = player:getpos()
			np = element_portals:teleport_landing_tricks({x=math.floor(np.x),y=math.floor(np.y),z=math.floor(np.z)},player)
			minetest.chat_send_player(player:get_player_name(), "You landed at "..minetest.pos_to_string(np))
		end,player)
	else 
		minetest.chat_send_player(player:get_player_name(), "The selected portal is disabled and it cannot be used")
	end
end

-- run a sanitize when player joins
minetest.register_on_joinplayer(function(player) 
  if player then
  	 element_portals:sanitize_player_portals(player)
  end 
end)
